// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading.Actors;
using System.Threading.Tasks;

namespace ActorFramework.Fakebook
{
    /*
     * The NewsFeed actor aggregates all of your friends' (and your) news posts, and publishes them
     * to the client.  This way, a client app only needs to subscribe to one actor for news posts, 
     * keeping the number of network connections from client to the data center down to a minimum.
     * Also, the NewsFeed actor needs to subscribe to a person's friends list.  This way if they add
     * or remove a friend, the NewsFeed actor receives an event telling it to subscribe or unsubscribe
     * from that new or ex-friend's news posts.
     * 
     * One curiousity - I have built this actor using an empty actor and by uploading an assembly
     * to provide the implementation.  That is useful to an extent, but does mean each client needs
     * to carry around a server-compatible (ie x64 loadable) version of this assembly.  While this does
     * show off the flexibility of an empty actor, this may not be a good approach for a more complete 
     * system.  Instead, deploying the service uniquely to the server may be a better idea.
     * 
     * Provides the following events for clients:
     *     NewNews
     * 
     * Makes use of the following events from other actors:
     *     Collection.ElementAdded
     *     Collection.ElementRemoved
     * 
     * Rest of Fakebook relies on other actors:
     *     Person actor - represents a user, their profile information, and a photo of themselves.
     *     Friends List - a separate CloudList<String>
     *     Photos - a CloudStringDictionary<PictureInfo>
     *     News Posts - a CloudList<String>
     */
    public static class NewsFeedActor
    {
        private const String CloudListActorPrefix = "fabric:/actor/list/";

        // For the name of a CloudList<String> for Friends and for News Posts
        private const String FriendsListName = CloudListActorPrefix + "Friends";
        private const String NewsPostsListName = CloudListActorPrefix + "NewsPosts";

        // For the nameof a key within this actor
        private static String SubscriptionsKey(String accountName)
        {
            return accountName + "_Subscriptions";
        }

        // This method ensures that a Newsfeed is set up appropriately.
        //     Subscribes to our friends list, to get notifications for new friends and removed friends.
        //     Subscribes to each of our friends' news posts.
        [ActorMethod]
        public static Object TryConstruct(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            String accountName = (String) parameters[0];
            String subscriptionsKeyName = SubscriptionsKey(accountName);
            Object obj;

            if (state.TryGet(subscriptionsKeyName, out obj))
            {
                // Already initialized.  Return.
                return null;
            }

            // Subscribe to my friends list, so I get updates.
            IActorProxy friendsList = state.GetActorProxy(FriendsListName, accountName);
            friendsList["Collection.ElementAdded"].Subscribe("NewFriendHandler");
            friendsList["Collection.ElementRemoved"].Subscribe("UnfriendHandler");

            // Enumerate all friends, then subscribe to each of their newsfeeds.
            IActorPromise friendsPromise = friendsList.Request("Enumerate", new Object[] { "Friends", 0, -1 });
            String[] friends = friendsPromise.GetTypedResult<String[]>();

            Dictionary<String, IDisposable> subscriptions = new Dictionary<String, IDisposable>();
            foreach (String friend in friends)
            {
                IActorProxy friendNewsfeed = state.GetActorProxy(NewsPostsListName, friend);
                IActorStateObservable observable = friendNewsfeed["Collection.ElementAdded"];
                subscriptions.Add(friend, observable.Subscribe("NewNewsHandler"));
            }
            
            // Facebook includes your own news posts in the news feed.
            IActorProxy myNewsfeed = state.GetActorProxy(NewsPostsListName, accountName);
            IActorStateObservable myObservable = myNewsfeed["Collection.ElementAdded"];
            subscriptions.Add(accountName, myObservable.Subscribe("NewNewsHandler"));

            state.Set(subscriptionsKeyName, subscriptions);

            return null;
        }

        [ActorMethod]
        public static Object NewFriendHandler(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            ActorEvent newFriendEvent = (ActorEvent)parameters[0];
            String accountName = newFriendEvent.EventSourcePartitionName;
            String newFriend = newFriendEvent.GetTypedPayload<String>();
            String subscriptionsKeyName = SubscriptionsKey(accountName);

            // Get current subscriptions
            Dictionary<String, IDisposable> subscriptions = null;
            Object obj = null;
            if (state.TryGet(subscriptionsKeyName, out obj))
                subscriptions = (Dictionary<String, IDisposable>)obj;
            else
                subscriptions = new Dictionary<String, IDisposable>();

            // If we're already subscribed, dispose of that.
            IDisposable oldSubscription = null;
            if (subscriptions.TryGetValue(newFriend, out oldSubscription))
            {
                oldSubscription.Dispose();
                subscriptions.Remove(newFriend);
            }

            // Subscribe to the new friend's news posts
            IActorProxy friendNewsPosts = state.GetActorProxy(NewsPostsListName, newFriend);
            IActorStateObservable observable = friendNewsPosts["Collection.ElementAdded"];
            subscriptions.Add(newFriend, observable.Subscribe("NewNewsHandler"));

            // Update Subscriptions dictionary in the actor
            state.Set(subscriptionsKeyName, subscriptions);

            return null;
        }

        [ActorMethod]
        public static Object UnfriendHandler(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            ActorEvent exFriendEvent = (ActorEvent)parameters[0];
            String accountName = exFriendEvent.EventSourcePartitionName;
            String exFriend = exFriendEvent.GetTypedPayload<String>();
            String subscriptionsKeyName = SubscriptionsKey(accountName);

            // Get current subscriptions
            Dictionary<String, IDisposable> subscriptions = null;
            Object obj = null;
            if (!state.TryGet(subscriptionsKeyName, out obj))
                return null;  // No current subscription
            subscriptions = (Dictionary<String, IDisposable>)obj;

            // If we're subscribed, dispose of that.
            IDisposable oldSubscription = null;
            if (subscriptions.TryGetValue(exFriend, out oldSubscription))
            {
                oldSubscription.Dispose();
                // Remove subscription from our store of current subscriptions.
                subscriptions.Remove(exFriend);
            }

            // Update Subscriptions dictionary in the actor
            state.Set(subscriptionsKeyName, subscriptions);

            return null;
        }

        [ActorMethod]
        public static Object NewNewsHandler(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            ActorEvent publishedNews = (ActorEvent)parameters[0];

            String accountName = publishedNews.EventSourcePartitionName;
            String newsPosterAccountName = accountName;
            String newsItem = publishedNews.EventPayload;

            state.Publish("NewNews", newsPosterAccountName, newsItem);
            return null;
        }
    }
}
